package com.svconnect.plugin.readers;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import com.svconnect.plugin.model.wsdl.ComplexDataType;
import com.svconnect.plugin.model.wsdl.EComplexType;
import com.svconnect.plugin.model.wsdl.Endpoint;
import com.svconnect.plugin.model.wsdl.IDataType;
import com.svconnect.plugin.model.wsdl.Operation;
import com.svconnect.plugin.model.wsdl.Parameter;
import com.svconnect.plugin.model.wsdl.Service;
import com.svconnect.plugin.model.wsdl.SimpleDataType;
import com.svconnect.plugin.util.Log;

public class CopyOfWSDLParser {

	private String url;

	private Document wsdlDoc;
	private Map<String, Document> xsdSchemas = new HashMap<String, Document>();
	private Map<String, Document> wsdlDefinitions = new HashMap<String, Document>();

	private Map<String, IDataType> resolvedTypes;
	private WSDLSerialization serialization;

	public CopyOfWSDLParser(String url) {
		this.url = url;
		this.resolvedTypes = new HashMap<String, IDataType>();
		this.serialization = new WSDLSerialization();
	}

	public String getUrl() {
		return url;
	}

	public List<Service> parse() throws IOException, SAXException,ParserConfigurationException {
		this.importWsdlDefinitions();
		this.importXsdSchemas();
		return this.getServices();
	}

	private void importWsdlDefinitions() throws IOException, SAXException, ParserConfigurationException {
		// get main wsdl document
		this.wsdlDoc = this.getXmlDocument(url);
		String ns = this.getNodeAttribute(wsdlDoc.getDocumentElement(), "targetNamespace");
		wsdlDefinitions.put(ns, wsdlDoc);
		
		// import wsdl parts defined in main wsdl file
		NodeList wsdlImports = this.wsdlDoc.getElementsByTagName("wsdl:import");
		int count = wsdlImports.getLength();
		for(int i=0; i<count; i++) {
			String location = this.getNodeAttribute(wsdlImports.item(i), "location");
			Document wsdlPart = this.getXmlDocument(location);
			String partNs = this.getNodeAttribute(wsdlPart.getDocumentElement(), "targetNamespace");
			wsdlDefinitions.put(partNs, wsdlPart);
		}
	}
	
	private void importXsdSchemas() throws IOException, SAXException, ParserConfigurationException {
		// import xsd schemas from every wsdl file
		for(Document doc : wsdlDefinitions.values()) {
			this.importXsdSchemas(doc);
		}
	}
	
	private void importXsdSchemas(Document doc) throws IOException, SAXException, ParserConfigurationException {
		// recursively import xsd schemas
		NodeList nodes = doc.getElementsByTagName("xsd:import");
		int count = nodes.getLength();
		for (int i = 0; i < count; i++) {
			Node node = nodes.item(i);
			String schemeLocation = this.getNodeAttribute(node,"schemaLocation");
			String ns = this.getNodeAttribute(node, "namespace");
			if (!xsdSchemas.containsKey(ns)) {
				Document xsdScheme = this.getXmlDocument(schemeLocation);
				this.xsdSchemas.put(ns,xsdScheme);
				this.importXsdSchemas(xsdScheme);
			}
		}		
	}
	private Document getXmlDocument(String url) throws IOException,
			SAXException, ParserConfigurationException {

		Log.i("Fetching " + url);
		DocumentBuilderFactory domFactory = DocumentBuilderFactory
				.newInstance();
		domFactory.setNamespaceAware(true);
		DocumentBuilder builder = domFactory.newDocumentBuilder();
		Document d = builder.parse(url);
		return d;
	}

	private List<Service> getServices() {
		NodeList nodes = this.wsdlDoc.getElementsByTagName("wsdl:service");
		List<Service> result = new LinkedList<Service>();
		int count = nodes.getLength();
		Log.i("Found " + count + " service(s)");
		for (int i = 0; i < count; i++) {
			Element serviceNode = (Element) nodes.item(i);
			String serviceName = this.getNodeAttribute(serviceNode, "name");
			
			Node portNode = serviceNode.getElementsByTagName("wsdl:port").item(0);
			String bindingName = this.getNodeAttribute(portNode, "name");
			
			Node addressNode = serviceNode.getElementsByTagName("soap:address").item(0);
			String location = this.getNodeAttribute(addressNode, "location");

			Endpoint endpoint = new Endpoint();
			endpoint.setAddress(location);
			endpoint.setName(bindingName);
			endpoint.setBinding("basicHttpBinding");
			
			Service svc = new Service();
			svc.setName(serviceName);
			svc.setEndpoint(endpoint);
			
			result.add(svc);
			
			Log.i("Processing " + bindingName + " binding for service "+ svc.toString());
			svc.setOperations(this.getOperations(svc, bindingName));
		}

		return result;
	}

	private List<Operation> getOperations(Service svc, String bindingName) {
		Element bindingElem = this.getWsdlElement("wsdl:binding","name", bindingName);
		String typeName = this.getNodeAttribute(bindingElem, "type");
		typeName = this.removeNamespace(typeName);
		
		Element portTypeElem = this.getWsdlElement("wsdl:portType","name", typeName);
		String tns = this.getNodeAttribute(portTypeElem, "targetNamespace");
		svc.setNamespace(tns);
		
		NodeList operations = portTypeElem.getElementsByTagName("wsdl:operation");
		int count = operations.getLength();
		Log.i("Found " + count + " operation(s)");

		List<Operation> result = new LinkedList<Operation>();
		for (int i = 0; i < count; i++) {
			Element operationElem = (Element) operations.item(i);
			String operationName = this.getNodeAttribute(operationElem, "name");
			Log.i("Processing '" + operationName + "' operation");

			String inputMessageName = null;
			String outputMessageName = null;
			List<String> faultMessageNames = new ArrayList<String>();
			
			String action = null;
			NodeList children = operationElem.getChildNodes();
			for (int j = 0; j < children.getLength(); j++) {
				Node child = children.item(j);
				String nodeName = child.getNodeName();
				if (nodeName.equals("wsdl:input")) {
					action = this.getNodeAttribute(child, "wsaw:Action");
					inputMessageName = this.getNodeAttribute(child, "message");
				} else if (nodeName.equals("wsdl:output")) {
					outputMessageName = this.getNodeAttribute(child, "message");
				} else if (nodeName.equals("wsdl:fault")) {
					faultMessageNames.add(this.getNodeAttribute(child, "message"));
				}
				
			}
			if (inputMessageName != null && outputMessageName != null && action!=null) {
				Operation op = new Operation();
				op.setName(operationName);
				op.setAction(action);
				op.setParameters(this.getOperationParameters(removeNamespace(inputMessageName)));
				op.setResponse(this.getOperationResponseType(removeNamespace(outputMessageName)));
				for(String faultMsg : faultMessageNames) {
					op.addFault(this.getOperationFault(removeNamespace(faultMsg)));
				}
				result.add(op);
			}
		}
		return result;
	}
	
	private ComplexDataType getOperationFault(String faultMsg) {
		String elementType = getMessageElementType(faultMsg,"detail");
		if (elementType != null) {
			Element desc = this.getXsdElement("xs:element", "name",elementType);
			IDataType type = this.resolveType(desc);
			((ComplexDataType)type).setType(EComplexType.EXCEPTION);
			return (ComplexDataType)type;
		}
		return null;
	}

	private List<Parameter> getOperationParameters(String messageName) {
		String elementType = getMessageElementType(messageName,"parameters");
		if (elementType != null) {
			Element desc = this.getXsdElement("xs:element", "name",
					elementType);
			NodeList elemList = desc.getElementsByTagName("xs:sequence")
					.item(0).getChildNodes();
			List<Parameter> parameters = new LinkedList<Parameter>();
			for (int i = 0; i < elemList.getLength(); i++) {
				Element elem = (Element) elemList.item(i);
				String paramName = this.getNodeAttribute(elem, "name");
				Parameter param = new Parameter();
				param.setName(paramName);
				IDataType tp = this.resolveType(elem);
				param.setType(tp);
				parameters.add(param);
			}
			return parameters;
		}
		return null;
	}

	private IDataType getOperationResponseType(String messageName) {
		String elementType = getMessageElementType(messageName,"parameters");
		if (elementType != null) {
			Element desc = this.getXsdElement("xs:element", "name",elementType);
			NodeList elemList = desc.getElementsByTagName("xs:sequence")
					.item(0).getChildNodes();

			if (elemList.getLength() == 0) {
				return SimpleDataType.VOID;
			} else {
				Element elem = (Element) elemList.item(0);
				IDataType tp = this.resolveType(elem);
				return tp;
			}
		}
		return null;
	}

	private IDataType resolveType(Element xsElem) {
		String name = this.getNodeAttribute(xsElem, "name");
		String typeName = null;

		String namespace = this.getNodeAttribute(xsElem, "targetNamespace");
		
		Element complexTypeElem = null;
		if (xsElem.hasChildNodes()) {
			typeName = name;
			NodeList nodes = xsElem.getElementsByTagName("xs:complexType");
			if (nodes.getLength() > 0) {
				complexTypeElem = (Element) nodes.item(0);
			}
		} else {
			typeName = this.getNodeAttribute(xsElem, "type");
			boolean isBuiltIn = typeName.startsWith("xs");
			typeName = this.removeNamespace(typeName);

			if (!isBuiltIn) {
				complexTypeElem = this.getXsdElement("xs:complexType",
						"name", typeName);
				try {
					namespace = this.getNodeAttribute(complexTypeElem, "targetNamespace");
				}
				catch(Exception ex) {}
			}
		}


		if (complexTypeElem != null) {
			if (!resolvedTypes.containsKey(typeName)) {
				
				
				ComplexDataType dt = new ComplexDataType();
				dt.setNamespace(namespace);
				resolvedTypes.put(typeName, dt);
				
				// get base class if one exists
				NodeList extNodes = complexTypeElem.getElementsByTagName("xs:extension");
				if (extNodes.getLength()>0) {
					String baseClsName = this.getNodeAttribute(extNodes.item(0), "base");
					if (baseClsName!=null) {
						baseClsName = this.removeNamespace(baseClsName);
						Element el = this.getXsdElement("xs:element", "name", baseClsName);
						if (el!=null) {
							IDataType baseType = this.resolveType(el);
							dt.setBaseType((ComplexDataType)baseType);
						}
					}
				}
				
				NodeList typeElements = null;
				NodeList nl = complexTypeElem.getElementsByTagName("xs:sequence");
				typeElements = nl.item(0).getChildNodes();

				int count = typeElements.getLength();
				dt.setName(serialization.getJavaTypeName(typeName));

				Element node = (Element)typeElements.item(0);
				String maxOccurs = this.getNodeAttribute(node, "maxOccurs");
				if (maxOccurs == null)
					maxOccurs = "1";				
				
				if (count == 1 && maxOccurs.equals("unbounded")) {
						
						Parameter p = new Parameter();
						p.setName("value");
						
						node.setAttribute("targetNamespace", namespace);
						IDataType tp = this.resolveType(node);
						p.setType(tp);
						dt.addMember(p);

						NodeList nlist = complexTypeElem
								.getElementsByTagName("IsDictionary");
						if (nlist.getLength() > 0) {
							dt.setType(EComplexType.DICTIONARY);
							ComplexDataType keyValPair = (ComplexDataType)dt.getMembers().get(0).getType();
							for(Parameter pa : keyValPair.getMembers()) {
								if (pa.getType() instanceof SimpleDataType) {
									SimpleDataType sdt = (SimpleDataType)pa.getType();
									sdt.setNullable(false);
									sdt.setName(serialization.getJavaTypeName(sdt.getName()));
								}
							}
						} else {
							dt.setType(EComplexType.SEQUENCE);
							IDataType innerType = dt.getMembers().get(0).getType();
							if (innerType instanceof SimpleDataType) {
								SimpleDataType sdt = (SimpleDataType)innerType;
								sdt.setNullable(false);
								sdt.setName(serialization.getJavaTypeName(sdt.getName()));
							}
						}

				} else {
					for (int i = 0; i < count; i++) {
						node = (Element) typeElements.item(i);
						Parameter p = new Parameter();
						p.setName(this.getNodeAttribute(node, "name"));
						IDataType tp = this.resolveType(node);
						p.setType(tp);
						dt.addMember(p);
					}
				}
				return dt;

			} else {
				return resolvedTypes.get(typeName);
			}
		} else {
			String nullableStr = xsElem.getAttribute("nillable");
			boolean nullable = (nullableStr.equals("true"));
			
			Element elem = this.getXsdElement("xs:simpleType", "name",typeName);
			if (elem != null) {
				NodeList enums = elem.getElementsByTagName("xs:enumeration");
				if (enums.getLength() > 0) {
					ComplexDataType dt = new ComplexDataType();
					dt.setName(typeName);
					dt.setType(EComplexType.ENUMERATION);
					for (int i = 0; i < enums.getLength(); i++) {
						Node enumNode = enums.item(i);
						String value = this.getNodeAttribute(enumNode, "value");
						Parameter p = new Parameter();
						p.setName(value);
						dt.addMember(p);
					}
					return dt;
				}
			}

			SimpleDataType dt = new SimpleDataType();
			dt.setNullable(nullable);
			dt.setName(serialization.getJavaTypeName(typeName,nullable));
			return dt;
		}
	}

	private String getMessageElementType(String messageName, String messagePartName) {
		Element messageElem = this.getWsdlElement("wsdl:message",
				"name", messageName);
		NodeList children = messageElem.getChildNodes();
		for (int i = 0; i < children.getLength(); i++) {
			Node node = children.item(i);
			if (this.getNodeAttribute(node, "name").equals(messagePartName)) {
				String elementType = removeNamespace(this.getNodeAttribute(node, "element"));
				return elementType;
			}
		}
		return null;
	}


	private Element getElementByAttribute(Document doc, String tagName,
			String attrName, String attrValue) {
		NodeList bindings = doc.getElementsByTagName(tagName);
		for (int i = 0; i < bindings.getLength(); i++) {
			Element bnd = (Element) bindings.item(i);
			String attr = this.getNodeAttribute(bnd, attrName);

			if (attr != null && attr.equals(attrValue)) {
				bnd.setAttribute("targetNamespace", this.getNodeAttribute(doc.getDocumentElement(), "targetNamespace"));
				return bnd;
			}
		}
		return null;
	}
	
	private Element getWsdlElement(String tagName, String attrName, String attrValue) {
		for(Document doc : this.wsdlDefinitions.values()) {
			Element result = this.getElementByAttribute(doc, tagName, attrName, attrValue);
			if (result!=null) {
				return result;
			}
		}
		return null;
	}
	
	private Element getXsdElement(String tagName, String attrName, String attrValue) {
		for(Document doc : this.xsdSchemas.values()) {
			Element result = this.getElementByAttribute(doc, tagName, attrName, attrValue);
			if (result!=null) {
				return result;
			}
		}
		return null;		
	}

	private String getNodeAttribute(Node node, String attrName) {
		Attr nameAttr = (Attr) node.getAttributes().getNamedItem(attrName);
		return (nameAttr != null) ? nameAttr.getValue() : null;
	}
	
	private String removeNamespace(String str) {
		String[] splitted = str.split(":");
		return splitted[splitted.length-1];
	}
	
	
	
}
